import { Directive, ElementRef, EventEmitter, HostListener, Input, OnInit, Output } from '@angular/core';

export class GridDragEvent {
    public x: number;
    public y: number;
    public startX: number;
    public startY: number;
    public deltaX: number;
    public deltaY: number;
}

@Directive({
    selector: '[draggable]',
})
export class DraggableDirective implements OnInit {
    @Input() public dragger: string; // drag selector
    @Input() public draggable: boolean;
    @Input() public offsetParent: HTMLElement;
    @Output() public dragStart: EventEmitter<GridDragEvent> = new EventEmitter();
    @Output() public dragging: EventEmitter<GridDragEvent> = new EventEmitter();
    @Output() public dragEnd: EventEmitter<GridDragEvent> = new EventEmitter();
    private startPos: {x: number, y: number};
    private lastPos: {x: number, y: number};
    private isDrag: boolean = false;

    private prefixes = ['Moz', 'Webkit', 'O', 'ms'];
    // Useful for preventing blue highlights all over everything when dragging.
    private userSelectPrefix = this.getPrefix('user-select');
    private userSelect = this.browserPrefixToStyle('user-select', this.userSelectPrefix);
    private userSelectStyle = `;${this.userSelect}: none;`;
    private userSelectReplaceRegExp = new RegExp(`;?${this.userSelect}: none;`); // leading ; not present on IE

    constructor(private elementRef: ElementRef) { }

    public ngOnInit(): void {
        if (this.draggable !== false) {
            this.draggable = true;
        }
    }

    @HostListener('mousedown', ['$event'])
    public onMouseDown(ev: MouseEvent): void {
        if (this.draggable && (!this.dragger || this.matchesSelectorAndParentsTo(ev.target as Node, this.dragger, this.elementRef.nativeElement))) {
            let el: HTMLElement = this.elementRef.nativeElement;
            this.isDrag = true;
            this.lastPos = this.offsetXYFromParent(ev, this.offsetParent || el.offsetParent);
            this.startPos = {...this.lastPos};
            this.dragStart.next({...this.lastPos, startX: this.startPos.x, startY: this.startPos.y, deltaX: 0, deltaY: 0});
            this.addUserSelectStyles(el.ownerDocument.body);
        }
    }

    @HostListener('document:mousemove', ['$event'])
    public onMouseMove(ev: MouseEvent): void {
        if (this.isDrag) {
            let el: HTMLElement = this.elementRef.nativeElement;
            let old = this.lastPos;
            this.lastPos = this.offsetXYFromParent(ev, this.offsetParent || el.offsetParent);
            this.dragging.next({x: this.lastPos.x, y: this.lastPos.y,
                startX: this.startPos.x, startY: this.startPos.y,
                deltaX: this.lastPos.x - old.x, deltaY: this.lastPos.y - old.y});
        }
    }

    @HostListener('document:mouseup', ['$event'])
    public onMouseUp(ev: MouseEvent): void {
        if (this.isDrag) {
            let el: HTMLElement = this.elementRef.nativeElement;
            let old = this.lastPos;
            this.lastPos = this.offsetXYFromParent(ev, this.offsetParent || el.offsetParent);
            this.dragEnd.next({x: this.lastPos.x, y: this.lastPos.y,
                startX: this.startPos.x, startY: this.startPos.y,
                deltaX: this.lastPos.x - old.x, deltaY: this.lastPos.y - old.y});
            this.isDrag = false;
            this.removeUserSelectStyles(el.ownerDocument.body);
        }
    }

    private matchesSelectorAndParentsTo(el: Node, selector: string, baseNode: Node): boolean {
        let node = el;
        do {
            if (this.matchesSelector(node, selector)) {
                return true;
            }
            if (node === baseNode) {
                return false;
            }
            node = node.parentNode;
        } while (node);

        return false;
    }

    private matchesSelector(el: Node, selector: string): boolean {
        let func = ['matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector', 'oMatchesSelector']
            .find((method) => typeof el[method] === 'function' || Object.prototype.toString.call(el[method]) === '[object Function]');

        // $FlowIgnore: Doesn't think elements are indexable
        return el[func].call(el, selector);
    }

    private offsetXYFromParent(evt: {clientX: number, clientY: number}, offsetParent: Element): {x: number, y: number} {
        const isBody = offsetParent === offsetParent.ownerDocument.body;
        const offsetParentRect = isBody ? {left: 0, top: 0} : offsetParent.getBoundingClientRect();

        const x = evt.clientX + offsetParent.scrollLeft - offsetParentRect.left;
        const y = evt.clientY + offsetParent.scrollTop - offsetParentRect.top;

        return {x, y};
    }

    private addUserSelectStyles(body: HTMLElement) {
        const style = body.getAttribute('style') || '';
        if (this.userSelectReplaceRegExp.test(style)) {
            return;
        } // don't add twice
        body.setAttribute('style', style + this.userSelectStyle);
    }

    private removeUserSelectStyles(body: HTMLElement) {
        const style = body.getAttribute('style') || '';
        body.setAttribute('style', style.replace(this.userSelectReplaceRegExp, ''));
    }

    private getPrefix(prop: string = 'transform'): string {
        // Checking specifically for 'window.document' is for pseudo-browser server-side
        // environments that define 'window' as the global context.
        // E.g. React-rails (see https://github.com/reactjs/react-rails/pull/84)
        if (typeof window === 'undefined' || typeof window.document === 'undefined') {
            return '';
        }

        const style = window.document.documentElement.style;
        if (prop in style) {
            return '';
        }

        for (let p of this.prefixes) {
            if (this.browserPrefixToKey(prop, p) in style) {
                return p;
            }
        }

        return '';
    }

    private browserPrefixToKey(prop: string, prefix: string): string {
        return prefix ? `${prefix}${this.kebabToTitleCase(prop)}` : prop;
    }

    private browserPrefixToStyle(prop: string, prefix: string): string {
        return prefix ? `-${prefix.toLowerCase()}-${prop}` : prop;
    }

    private kebabToTitleCase(str: string): string {
        let out = '';
        let shouldCapitalize = true;
        for (let i = 0; i < str.length; i++) {
            if (shouldCapitalize) {
                out += str[i].toUpperCase();
                shouldCapitalize = false;
            } else if (str[i] === '-') {
                shouldCapitalize = true;
            } else {
                out += str[i];
            }
        }
        return out;
    }
}
